/*
 	Script: GearsORM.Model.js
 	        Contains GearsORM.Model a metaclass used to define models
 	
 	License:
 	        MIT-style license.
*/

/*
 Class: GearsORM.Model
 	a metaclass to define models that represent database tables.
  	a metaclass is a class in which the instances are also classes.
  	
 Constructor:
 
 Parameters:
 	
  options - a object used to describe the model look like this:
  { 
  	 name : "Name of the model table",
    fields: 
    {
    	name_of_the_field:a_instance_of_GearsORM.Fields.*
    }
   }
 */
GearsORM.Model = function(options)
{
	if(!options || !options.fields || !options.name)
	{
		throw new Error("model must have fields and name");
	}
	/*
	  Class: GearsORM.Model.model
	  
	  	represent a model instance
	  	
	  Constructor:
	  
	  Parameters:
	  	valuesOrRowID - (optional)a object used to initialize the model instance with values or
	  	it can be the id of the row,in that case it will do a select to retrive the model data.
	 */
	var model = function(valuesOrRowID)
	{
		this.modelClass = model;
		if(valuesOrRowID)
		{
			if(typeof(valuesOrRowID) == "number")
			{
				this.rowid = valuesOrRowID;
				this.refresh();
				return;
			}
			else
			{
				GearsORM.extend(this,valuesOrRowID);
			}
		}
		for(var fieldName in options.fields)
		{
			var field = options.fields[fieldName];
			if(field.isBackwardRelation)
			{
				var copiedField = this[fieldName] = field.copy();
				copiedField.instance = this;
				copiedField.modelClass = model;
			}
		}
			
	};
	model.options = options;
	model.prototype =
	{
		/*
		 Function: remove
		 
		 	remove the instance from the database
		 */
		remove:function()
		{
			if(this.rowid)
				GearsORM.executeAndClose(GearsORM.Sql.deleteRowByIdAndTable(options.name),[this.rowid]);
				
			this.rowid = null;
			return this;
		},
		/*
		 Function: save
		 
		 	save the instance to the database,if the instance is not in the database - it will be created.
		 	if it is already in the database it will be updated.
		 */
		save:function()
		{
			return this._updateInsert(!!this.rowid);
		},
		/*
		 Function: _updateInsert
		 
		 	do the actual insert/update,used by save method.
		 */
		_updateInsert:function(update)
		{
			var query = update ? GearsORM.Sql.updateByModel(model) : GearsORM.Sql.insertRowByModel(model);

			var values = [];
			var fields = model.options.fields;
			for(var fieldName in fields)
			{
				var field = fields[fieldName];
				if(!field.isBackwardRelation)
				{
					var value = this[fieldName];
					if(typeof(value) == "undefined")
						values.push(null);
					else if(!field.isRelation)
						values.push(value);
					else
					{
						if(typeof(value) == "number" || (!value && field.options.allowNull))
							values.push(value);
						else if(value.rowid)
							values.push(value.rowid);
						else
							throw new Error("value of related model can be a model instance,or a id(integer)");
					}	
				}
			}
			if(update)values.push(this.rowid);
			GearsORM.executeAndClose(query,values);
			if(!update)this.rowid = GearsORM.getDB().lastInsertRowId;
			return this;
		},
		/*
		 Function: refresh
		 
		 	refresh the model data from database.
		 */
		refresh:function()
		{
			
			if(this.rowid)
			{
				var result =model.select(options.name+".rowid = ?",[this.rowid]).result;
				if(!result.isValidRow())throw new Error("instance doesn`t exist in database");
				this._populateFromResult(result);
				result.close();
			}
			return this;
		},
		/*
		 Function: _populateFromResult
		 
		 	populate the instance from a ResultSet.
		 
		 Parameters:
		 
		 	result - a ResultSet that used to populate the instance.
		 */
		_populateFromResult:function(result)
		{
			var fields = options.fields;
			var name = options.name;
			//every table in google gears have a rowid
			this.rowid = result.fieldByName(name+"_rowid");
			for(var fieldName in fields)
			{
				var field = fields[fieldName];
				if(!field.isBackwardRelation)
				{
					if(field.isRelation)
					{
						var relatedClass = field.getRelatedClass();
						if(relatedClass == model)
							this[fieldName] = result.fieldByName(name+"_id");
						else
							this[fieldName] = (new relatedClass())._populateFromResult(result);
					}
					else
					{
						this[fieldName] = result.fieldByName(name+"_"+fieldName);
					}
				}
				else
				{
					var copiedField = this[fieldName] = field.copy();
					copiedField.instance = this;
					copiedField.modelClass = model;
				}
			}
			return this;
		}
	};
	//add reference to the model to the fields
	for(var fieldName in options.fields)
	{
		options.fields[fieldName].modelClass = model;
	}
	GearsORM.extend(model,this);
	GearsORM.extend(model,new GearsORM.Events);
	
	for(var i in model.prototype)//add events to save,remove,refresh
		if(i.charAt(0) != "_")//wrap only public functions
			model.prototype[i] = GearsORM.Events.wrapFunction(model.prototype[i],i.charAt(0).toUpperCase()+i.substring(1),model);

	model.constructor = GearsORM.Model;
	//register the model
	GearsORM._models[options.name] = model;
	return model;
};
/*
  class methods of GearsORM.Model 
  this reffer to the model class
 */
GearsORM.Model.prototype =
{
	/*
	 Function: count
	 
	 	do a COUNT on the model
	 
	 Parameters:
	 	
	 	whereExpression - a SQL expression to be used as the WHERE clause.
	 	params - bind parameters used in the SQL expression.
	 	
	 Returns:
	 
	 	how much objects(rows) match the where expression
	 */
	count:function(whereExpression,params)
	{
		var rs = GearsORM.execute(GearsORM.Sql.selectCount(this.options.name,whereExpression),params);
		var count = rs.fieldByName("c");
		rs.close();
		return count;
	},
	/*
	 Function: select
	 
	 	do a SELECT on the model
	 
	 Parameters:
	 	
	 	whereExpression - a SQL expression to be used as the WHERE clause.
	 	params - bind parameters used in the SQL expression.
	 	
	 Returns:
	 
	 	a new ResultIterator to iterate the ResultSet
	 */
	select:function(whereExpression,params)
	{
		return new GearsORM.ResultIterator(GearsORM.execute(GearsORM.Sql.selectWithForigenKeys(this,whereExpression),params),this);
	},
	/*
	 Function: remove
	 
	 	do a DELETE on the model
	 
	 Parameters:
	 	
	 	whereExpression - a SQL expression to be used as the WHERE clause.
	 	params - bind parameters used in the SQL expression.
	 */
	remove:function(whereExpression,params)
	{
		GearsORM.executeAndClose(GearsORM.Sql.deleteFromTableWhere(this.options.name,whereExpression),params);
	},
	/*
	 Function: createTable
	 
	 	create the table for this model,creating triggers for forigenkeys and creating many-to-many tables if needed.
	 */
	createTable:function()
	{
		GearsORM.Transaction(function(){
			GearsORM.executeAndClose(GearsORM.Sql.createTableByModelClass(this));
			var fields = this.options.fields;
			for(var fieldName in fields)
			{
				var field = fields[fieldName];
				if(field.createTable)
					field.createTable();
			}
			this.createTriggers();
		},this);
	},
	/*
	 Function: dropTable
	 
	 	drop the table and all associate triggers.
	 */
	dropTable:function()
	{
		GearsORM.Transaction(function(){
			GearsORM.executeAndClose(GearsORM.Sql.dropTableByName(this.options.name));
			this.dropTriggers();
		},this);
	},
	/*
	 Function: createTriggers
	 
	 	create triggers to inforce forigenkeys.
	 */
	createTriggers:function()
	{
		//in case of triggers in m2m tables check first that tables exist
		if(this.options.name.indexOf("m2m") == 0)
		{
			//find out which tables are related to this table
			var tableNames = [];
			for(var i in this.options.fields)
			{
				tableNames.push(this.options.fields[i].options.related);
			}
			//if one of the tables doesn`t exist don`t create the triggers
			if(!GearsORM.Introspection.doesTableExist.apply(this,tableNames))
				return;
		};			
		var fields = this.options.fields;
		var name = this.options.name;
		GearsORM.Transaction(function(){
			for(var fieldName in fields)
			{
				var field = fields[fieldName];
				if(field.isRelation && !field.isBackwardRelation)
				{
					//on insert,check that forigen key is valid since sqlite doesn`t enforce forigenkeys
					GearsORM.executeAndClose(GearsORM.Sql.createInsertForigenKeyTrigger(name,field.getRelatedClass().options.name,field.options.allowNull));
					//on update,check that forigen key is valid	since sqlite doesn`t enforce forigenkeys
					GearsORM.executeAndClose(GearsORM.Sql.createUpdateForigenKeyTrigger(name,field.getRelatedClass().options.name,field.options.allowNull));
					
					if(field.options.onDeleteCascade)
						//on delete from parent table,delete all rows that reference the row that is going to be deleted
						GearsORM.executeAndClose(GearsORM.Sql.createDeleteCascadeTrigger(name,field.getRelatedClass().options.name));
					else
						//on delete from parent table,if there is a row that reference the row that is going to be deleted throw a exception
						GearsORM.executeAndClose(GearsORM.Sql.createDeleteRestrictTrigger(name,field.getRelatedClass().options.name));
				}
			}
		});
	},
	/*
	 Function: dropTriggers
	 
	 	drop triggers related to the table.
	 */
	dropTriggers:function()
	{
		var fields = this.options.fields;
		var name = this.options.name;
		GearsORM.Transaction(function(){
			for(var fieldName in fields)
			{
				var field = fields[fieldName];
				if(field.isRelation && !field.isBackwardRelation)
				{
					//INSERT
					GearsORM.executeAndClose(GearsORM.Sql.dropForigenKeyInsertTrigger(name,field.getRelatedClass().options.name));
					//UPDATE
					GearsORM.executeAndClose(GearsORM.Sql.dropForigenKeyUpdateTrigger(name,field.getRelatedClass().options.name));
					//DELETE
					GearsORM.executeAndClose(GearsORM.Sql.dropForigenKeyDeleteTrigger(name,field.getRelatedClass().options.name));
				}
				else if(field._m2mModel)
				{
					field._m2mModel.dropTriggers();
				}
			}
		});
	},
	/*
	 Function: load
	 
	 	load objects from a array to the database.
	 	
	 Parameters:
	 	objects - a array of objects or one object to be saved in the database.
	 	save - if set to true all the objects will be saved to the database.
	 
	 Returns:
	 	return all the objects mapped using the model class.
	 */
	load:function(objects,save)
	{
		var mappedObjects = [],modelClass = this;
		if(!objects.length)
			objects = [objects];
		function map()
		{
			for(var objIndex=0,objsLength = objects.length;objIndex<objsLength;objIndex++)
			{
				var obj = new modelClass(objects[objIndex]);
				if(save)obj.save();
				mappedObjects.push(obj);
			}
		}
		if(save)GearsORM.Transaction(map);
		else map();
		return mappedObjects;
	}
};